<!--
@description: 普通日历
@author: ldb
@time: 2022-09-26 21:34:28
-->
<template>
  <div class="light-calendar">
    <div class="calendar-header">
      <slot name="header">
        <a-date-picker
          v-model:value="value"
          :format="'YYYY年MM月DD日'"
          :value-format="'YYYY-MM-DD'"
          :allow-clear="false"
          :default-picker-value="dayjs()"
          show-now
          show-today
          @change="handlerUpdateDate"
        />
      </slot>
    </div>
    <table class="calendar-table">
      <tr>
        <th
          v-for="(item, index) in header"
          :key="index"
          :style="{
            border: border == true ? borderStyle.hasBorder : borderStyle.unHasBorder,
          }"
        >
          {{ item }}
        </th>
      </tr>
      <tr v-for="(weeks, index) in showData" :key="index">
        <td
          v-for="(week, index) in weeks"
          :key="'week' + index"
          :style="{
            border: border == true ? borderStyle.hasBorder : borderStyle.unHasBorder,
          }"
          @click="handlerUpdateDate(DateToStr(week.date), week)"
        >
          <div class="current-day" :title="DateToStr(week.date, 'YYYY年MM月DD日')">
            <slot name="day" :data="week">
              <div
                class="default-current-day"
                :class="{
                  active: DateToStr(week.date) == value,
                  supplementDate: week.isSupplementDate == true,
                }"
              >
                {{ DateToStr(week.date, 'DD', true) }}
              </div>
              <!--交叉任务-->
              <template v-if="week.crossDay.length > 0">
                <div
                  v-for="(item, index) in week.crossDay"
                  :key="index"
                  :style="getCrossStyle(item, week.date)"
                  class="cross-day"
                  @click="$emit('selectChange', item)"
                >
                  {{ item.name }}
                </div>
              </template>
            </slot>
          </div>
        </td>
      </tr>
    </table>
  </div>
</template>

<script lang="ts" setup>
import { ref, onMounted, computed, defineEmits, defineProps, withDefaults } from 'vue'
import dayjs, { Dayjs } from 'dayjs'
import getRandomColor from '@/utils/color'
// 组件内数据
export interface CalendarValue {
  // 当前日期(实际日期)
  readonly date: Dayjs
  // 是否补充日期
  isSupplementDate?: boolean
  // 当天日期数据
  data?: any
  // 匹配时间
  matchDay?: Array<MatchDay>
  // 交叉时间
  crossDay?: Array<CrossDay>
}

/**
 * 匹配天数日期
 */
export interface MatchDay {
  time: Dayjs
  value?: unknown
}

/**
 * 匹配的交叉日期
 */
export interface CrossDay {
  startTime: Dayjs
  stopTime: Dayjs
  color?: string
  name?: string
  value?: unknown
}

/**
 * 日历属性
 */
interface CalendarProps {
  // 是否需要边框
  border?: boolean
  // 头部星期样式
  header?: Array<string>
  // 匹配的日期
  matchDays?: () => Array<MatchDay>
  // 交叉时间段
  crossDays?: () => Array<CrossDay>
}
/**
 * 前后需要补充的天数
 */
export interface FillDays {
  // 之前需填充的天数
  previousDay: number
  // 之后需填充的天数
  nextDay: number
}
// 边框样式
const borderStyle = {
  hasBorder: '1px solid rgba(0, 0, 0, 0.1)',
  unHasBorder: '0px solid rgba(0, 0, 0, 0.1)',
}
// 获取交叉样式
function getCrossStyle(data: CrossDay, currentTime: Dayjs) {
  if (!data.color) {
    data.color = getRandomColor()
  }
  const { startTime, stopTime } = data
  let flag = 1
  if (startTime.isSame(currentTime, 'day')) {
    flag = 0
  }
  if (stopTime.isSame(currentTime, 'day')) {
    flag = 2
  }
  // 开始样式 左边有圆角 中间样式 左右都没有圆角 右边样式 右边有圆角
  const radiusStyle = [
    {
      borderTopLeftRadius: '7px',
      borderBottomLeftRadius: '7px',
    },
    { borderRadius: '0px' },
    {
      borderBottomRightRadius: '7px',
      borderTopRightRadius: '7px',
    },
  ]
  return { backgroundColor: data.color, ...radiusStyle[flag] }
}
// 属性
const props = withDefaults(defineProps<CalendarProps>(), {
  border: true,
  header: () => ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'],
  matchDays: () => [],
  crossDays: () => [],
})
// 事件 change:时间改变时 selectChange:交叉事件点击改变时
const emits = defineEmits<{
  (name: 'change', value: CalendarValues): void
  (name: 'selectChange', value: CrossDay): void
}>()
// 日历数据
export type CalendarValues = Array<CalendarValue>
// 用户当前选择的时间
const value = ref<String>('')
// 上一月需要补充的天数
const previousDays = ref<CalendarValues>([])
// 下一月需要补充的天数
const nextDays = ref<CalendarValues>([])
// 本月数据
const currentMonth = ref<CalendarValues>([])
// 上月数据
const preMonth = ref<CalendarValues>([])
// 下月数据
const nextMonth = ref<CalendarValues>([])
onMounted(() => {
  // 日期值初始化
  value.value = DateToStr(dayjs())
  dateInit()
})
// 列表上最终展现的数据(5行7列)
const showData = computed(() => {
  const data = new Array<Array<CalendarValue>>()
  // 先将日期合并
  const values = previousDays.value.concat(currentMonth.value).concat(nextDays.value)
  const count = Math.round(values.length / 7)
  for (let i = 0; i < count; i++) {
    data.push(values.splice(0, 7))
  }
  return data
})

/**
 * 日期数据初始化
 */
function dateInit(now: Dayjs = dayjs()) {
  // 本月
  currentMonth.value = getMonthData(now)
  // 上月
  preMonth.value = getMonthData(now.subtract(1, 'month'))
  // 下月
  nextMonth.value = getMonthData(now.add(1, 'month'))
  // 更新补充的日期天数
  updateFillDays(currentMonth.value[0].date)
  // 更新匹配、交叉时间
  updateMatchAndCrossData()
}

/**
 * 更新匹配、交叉时间
 */
function updateMatchAndCrossData() {
  // 上月
  previousDays.value.map((val) => {
    dateTimeMatch(val)
    return val
  })
  // 本月
  currentMonth.value.map((val) => {
    dateTimeMatch(val)
    return val
  })
  // 下月
  nextDays.value.map((val) => {
    dateTimeMatch(val)
    return val
  })
}

/**
 * 时间匹配/交叉
 * @param data
 */
function dateTimeMatch(data: CalendarValue) {
  const matchDays = props.matchDays as unknown as Array<MatchDay>
  const crossDays = props.crossDays as unknown as Array<CrossDay>
  data.matchDay = matchDays.filter((val) => DateToStr(val.time) == DateToStr(data.date))
  // 等于开始时间 | 等于结束时间 | 开始之后 & 结束之前
  data.crossDay = crossDays.filter((val) => {
    const time = data.date
    return (
      time.isSame(val.startTime, 'day') ||
      time.isSame(val.stopTime, 'day') ||
      (time.isBefore(val.stopTime, 'day') && time.isAfter(val.startTime, 'day'))
    )
  })
}

/**
 * 更新前、后需要填充的天数
 * @param date 当月第一天
 */
function updateFillDays(date: Dayjs): void {
  const { previousDay, nextDay } = getFillDays(date)
  // slice:仅截取原有数据 splice:改变原有数组
  previousDays.value = preMonth.value.slice(preMonth.value.length - previousDay)
  nextDays.value = nextMonth.value.slice(0, nextDay)
  // 补充日期标识
  previousDays.value = previousDays.value.map((val) => {
    val.isSupplementDate = true
    return val
  })
  nextDays.value = nextDays.value.map((val) => {
    val.isSupplementDate = true
    return val
  })
}

/**
 * 获取需要补充的天数
 * @param date
 */
function getFillDays(date: Dayjs): FillDays {
  // 总天数
  const total = 42
  // 当月总天数
  const curMonthDays = date.daysInMonth()
  // 当月第一天是星期几 0为星期日
  const firstDay = date.day()
  let next = total - (curMonthDays + (firstDay - 1))
  return <FillDays>{
    previousDay: firstDay == 0 ? 6 : firstDay - 1,
    nextDay: firstDay == 0 ? next - 6 : next,
  }
}
/**
 * 日期更新事件
 * @param val 日期值
 * @param data 数据
 */
function handlerUpdateDate(val: string, data: CalendarValues) {
  value.value = val
  // 更新时间
  dateInit(StrToDayjs(val))
  // console.log(data)
  // 发出事件交给父组件处理
  emits('change', data)
}

/**
 * 格式化日期转Dayjs
 * @param str 格式化日期
 */
function StrToDayjs(str: string): Dayjs {
  // 解析值
  const dateArr = str.split('-') as unknown as Array<number>
  if (dateArr.length == 3) {
    return dayjs(new Date(dateArr[0], dateArr[1] - 1, dateArr[2]))
  } else {
    throw new Error('只能解析YYYY-MM-DD格式字符串!')
  }
}
/**
 * 月份切换
 * @param type 1=上一月 2=下一月 3=上一年 4=下一年
 */
function handlerMonthChange(type: number) {
  if (type == 1) {
    // 上一月
    value.value = DateToStr(StrToDayjs(value.value as string).subtract(1, 'month'))
  }
  if (type == 2) {
    // 下一月
    value.value = DateToStr(StrToDayjs(value.value as string).add(1, 'month'))
  }
  if (type == 3) {
    // 上一年
    value.value = DateToStr(StrToDayjs(value.value as string).subtract(1, 'year'))
  }
  if (type == 4) {
    // 下一年
    value.value = DateToStr(StrToDayjs(value.value as string).add(1, 'year'))
  }
  dateInit(StrToDayjs(value.value as string))
}

/**
 * 获取指定日期当月每一天数据
 * @param data dayjs对象
 */
function getMonthData(date: Dayjs): Array<CalendarValue> {
  const list: Array<CalendarValue> = []
  // 获取本月天数
  const days = date.daysInMonth()
  // 获取本月第一天
  let firstDay = date.startOf('month')
  list.push({ date: firstDay })
  // 遍历累加天数
  for (let i = 1; i < days; i++) {
    const nextDay = firstDay.add(1, 'day')
    list.push({ date: nextDay })
    firstDay = nextDay
  }
  return list
}

/**
 * 格式化时间
 * @param date
 */
function DateToStr(date: Dayjs, format: string = 'YYYY-MM-DD', hiddenZero = false): string {
  const val = date.format(format)
  return hiddenZero ? (val.indexOf('0') == 0 ? val.substring(1) : val) : val
}
</script>

<style scoped lang="scss">
@import './style/Calendar.scss';
</style>
